﻿/************************************************************************

   Extended Silverlight Toolkit

   Copyright (C) 2010-2012 Xceed Software Inc.

   This program is provided to you under the terms of the Microsoft Public
   License (Ms-PL) as published at http://extendedsilverlight.codeplex.com/license

   Please purchase a commercial version of this toolkit if you are using
   it in a commercial product. The commercial versions support the project,
   and also include more features for each control, priority support,
   source code, updates, removed watermark, and a proprietary license.

   Visit http://xceed.com and follow @datagrid on Twitter.

  **********************************************************************/

using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Diagnostics;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Input;
using Xceed.Silverlight.Compatibility;
using Xceed.Silverlight.DataGrid.Utils;

namespace Xceed.Silverlight.DataGrid
{
  #region Metadata

  [TemplatePart( Name = "CellContentHost", Type = typeof( ContentControl ) )]

  [TemplateVisualState( GroupName = "EditingStates", Name = "NotBeingEdited" )]
  [TemplateVisualState( GroupName = "EditingStates", Name = "BeingEdited" )]
  [TemplateVisualState( GroupName = "EditingStates", Name = "Committing" )]

  [TemplateVisualState( GroupName = "ValidationStates", Name = "ValidData" )]
  [TemplateVisualState( GroupName = "ValidationStates", Name = "InvalidData" )]

  #endregion

  public abstract class EditableCell : Cell
  {
    #region Static Fields

    internal static readonly XceedRoutedEvent ValidationStateChangedEvent = new XceedRoutedEvent();
    internal static readonly XceedRoutedEvent EditedContentCommittedEvent = new XceedRoutedEvent();
    internal static readonly XceedRoutedEvent EditableCellMouseLeftButtonDownEvent = new XceedRoutedEvent();

    private static readonly string EditingStateUnchanged = "NotBeingEdited";
    private static readonly string EditingStateEdited = "BeingEdited";
    private static readonly string EditingStateCommitting = "Committing";

    private static readonly string ValidationStateValid = "ValidData";
    private static readonly string ValidationStateInvalid = "InvalidData";

    private static readonly string EditedContentPropertyName = "EditedContent";

    #endregion

    #region Constructor

    internal EditableCell()
    {
      this.DefaultStyleKey = typeof( EditableCell );

      m_emptyValidationErrors = new Lazy<ReadOnlyObservableCollection<CellValidationError>>(
                                  () => new ReadOnlyObservableCollection<CellValidationError>(
                                          new ObservableCollection<CellValidationError>() ) );

      m_keyHandler = new CellEditKeyHandler( this, KeyHandler.Default );
      m_contentElementManager = new ContentElementManager( this );

      this.SetBinding(
        EditableCell.ContentProperty,
        new Binding( EditableCell.EditedContentPropertyName )
        {
          Mode = BindingMode.TwoWay,
          Source = this
        } );
    }

    #endregion

    #region TextTrimming Property

    public TextTrimming TextTrimming
    {
      get
      {
        return m_textTrimming;
      }
      internal set
      {
        if( value != m_textTrimming )
        {
          m_textTrimming = value;

          this.RaisePropertyChanged( () => this.TextTrimming );
        }
      }
    }

    private TextTrimming m_textTrimming;

    #endregion

    #region ValidationErrors Property

    public ReadOnlyObservableCollection<CellValidationError> ValidationErrors
    {
      get
      {
        if( m_viewModel != null )
          return m_viewModel.ValidationErrors;

        return m_emptyValidationErrors.Value;
      }
    }

    private readonly Lazy<ReadOnlyObservableCollection<CellValidationError>> m_emptyValidationErrors;

    #endregion

    #region HasValidationErrors Property

    public bool HasValidationErrors
    {
      get
      {
        if( m_viewModel != null )
          return m_viewModel.HasValidationErrors;

        return false;
      }
    }

    #endregion

    #region HasRestrictiveValidationErrors Property

    public bool HasRestrictiveValidationErrors
    {
      get
      {
        if( m_viewModel != null )
          return m_viewModel.HasRestrictiveValidationErrors;

        return false;
      }
    }

    #endregion

    #region ValidationErrorToolTip Property

    public ToolTip ValidationErrorToolTip
    {
      get
      {
        return m_validationErrorToolTip;
      }
    }

    private void UpdateValidationErrorToolTip()
    {
      if( !this.HasValidationErrors )
      {
        m_validationErrorToolTip = null;
      }
      else
      {
        m_validationErrorToolTip = new ToolTip();
        m_validationErrorToolTip.Content = this.ValidationErrors;
        m_validationErrorToolTip.ContentTemplate = this.ValidationErrorToolTipContentTemplate;
      }

      this.RaisePropertyChanged( () => this.ValidationErrorToolTip );
    }

    private ToolTip m_validationErrorToolTip;

    #endregion

    #region ValidationErrorToolTipContentTemplate Property

    public static readonly DependencyProperty ValidationErrorToolTipContentTemplateProperty = DependencyProperty.Register(
      "ValidationErrorToolTipContentTemplate",
      typeof( DataTemplate ),
      typeof( EditableCell ),
      new PropertyMetadata( new PropertyChangedCallback( EditableCell.OnValidationErrorToolTipContentTemplateChanged ) ) );

    public DataTemplate ValidationErrorToolTipContentTemplate
    {
      get
      {
        return ( DataTemplate )this.GetValue( EditableCell.ValidationErrorToolTipContentTemplateProperty );
      }
      set
      {
        this.SetValue( EditableCell.ValidationErrorToolTipContentTemplateProperty, value );
      }
    }

    private static void OnValidationErrorToolTipContentTemplateChanged( DependencyObject sender, DependencyPropertyChangedEventArgs e )
    {
      var cell = sender as EditableCell;
      if( cell == null )
        return;

      if( cell.ValidationErrorToolTip != null )
      {
        cell.ValidationErrorToolTip.ContentTemplate = e.NewValue as DataTemplate;
      }
    }

    #endregion

    #region ValidationState Property

    public ValidationState ValidationState
    {
      get
      {
        return m_validationState;
      }
      private set
      {
        if( value != m_validationState )
        {
          m_validationState = value;

          this.RaisePropertyChanged( () => this.ValidationState );
        }
      }
    }

    private ValidationState m_validationState = ValidationState.Valid;

    #endregion

    #region ContentElement Internal Property

    internal EditableCellContentElement ContentElement
    {
      get
      {
        return m_contentElement;
      }
      set
      {
        if( value != m_contentElement )
        {
          m_contentElement = value;

          FrameworkElement oldContainer = this.GetContentElement( m_contentHost );
          FrameworkElement newContainer = this.GetContentElement( value );

          if( oldContainer != newContainer )
          {
            m_contentElementManager.Clear( oldContainer );
            m_contentElementManager.Prepare( newContainer );
          }

          this.SetContentElement( newContainer, true );

          this.RaisePropertyChanged( () => this.ContentElement );
        }
      }
    }

    private EditableCellContentElement m_contentElement;

    #endregion

    #region ViewModel Internal Property

    internal EditableCellViewModel ViewModel
    {
      get
      {
        return m_viewModel;
      }
      set
      {
        if( value == m_viewModel )
          return;

        if( m_viewModel != null )
        {
          m_viewModel.PropertyChanged -= new PropertyChangedEventHandler( this.OnViewModelPropertyChanged );
          m_viewModel.Releasing -= new EventHandler( this.OnViewModelReleasing );
        }

        m_viewModel = value;

        if( m_viewModel != null )
        {
          m_viewModel.PropertyChanged += new PropertyChangedEventHandler( this.OnViewModelPropertyChanged );
          m_viewModel.Releasing += new EventHandler( this.OnViewModelReleasing );
        }

        this.UpdateContent();
        this.UpdateEditingVisualState();
        this.UpdateValidationErrors();
        this.UpdateValidationErrorToolTip();

        this.RaisePropertyChanged( () => this.ViewModel );
      }
    }

    private void OnViewModelPropertyChanged( object sender, PropertyChangedEventArgs e )
    {
      EditableCellViewModel viewModel = ( EditableCellViewModel )sender;

      if( e.IsPropertyChanged( ( EditableCellViewModel item ) => item.Value ) )
      {
        this.EditedContent = viewModel.Value;
      }

      if( e.IsPropertyChanged( ( EditableCellViewModel item ) => item.EditingState ) )
      {
        this.UpdateEditingVisualState();
      }

      if( e.IsPropertyChanged( ( EditableCellViewModel item ) => item.HasValidationErrors ) )
      {
        this.UpdateValidationErrors();
        this.UpdateValidationErrorToolTip();
      }
      else if( e.IsPropertyChanged( ( EditableCellViewModel item ) => item.HasRestrictiveValidationErrors ) )
      {
        this.UpdateValidationErrors();
      }
    }

    private void OnViewModelReleasing( object sender, EventArgs e )
    {
      EditableCellViewModel viewModel = ( EditableCellViewModel )sender;

      Debug.Assert( viewModel == this.ViewModel );

      this.ViewModel = null;
    }

    private EditableCellViewModel m_viewModel;

    #endregion

    #region EditedContent Private Property

    private static readonly DependencyProperty EditedContentProperty = DependencyProperty.Register(
      EditableCell.EditedContentPropertyName,
      typeof( object ),
      typeof( EditableCell ),
      new PropertyMetadata( null, new PropertyChangedCallback( EditableCell.OnEditedContentChanged ) ) );

    private object EditedContent
    {
      get
      {
        return this.GetValue( EditableCell.EditedContentProperty );
      }
      set
      {
        this.SetValue( EditableCell.EditedContentProperty, value );
      }
    }

    private static void OnEditedContentChanged( DependencyObject sender, DependencyPropertyChangedEventArgs e )
    {
      EditableCell cell = ( EditableCell )sender;
      FrameworkElement container = cell.GetContentElement( cell.m_contentHost );
      if( container == null )
        return;

      ContentElementReadyState readyState = EditableCell.GetContentElementReadyState( container );
      if( ( readyState == null ) || ( !readyState.IsReady ) )
        return;

      cell.UpdateSource( e.NewValue );
    }

    #endregion

    #region ContentElementReadyState Private Attach Property

    private static readonly DependencyProperty ContentElementReadyStateProperty = DependencyProperty.RegisterAttached(
      "ContentElementReadyState",
      typeof( ContentElementReadyState ),
      typeof( EditableCell ),
      new PropertyMetadata( null ) );

    private static ContentElementReadyState GetContentElementReadyState( DependencyObject target )
    {
      return ( ContentElementReadyState )target.GetValue( EditableCell.ContentElementReadyStateProperty );
    }

    private static void SetContentElementReadyState( DependencyObject target, ContentElementReadyState value )
    {
      target.SetValue( EditableCell.ContentElementReadyStateProperty, value );
    }

    private static void ClearContentElementReadyState( DependencyObject target )
    {
      target.ClearValue( EditableCell.ContentElementReadyStateProperty );
    }

    #endregion

    #region Public Editing Methods

    public bool BeginEdit()
    {
      var e = new XceedRoutedEventRequestEditEventArgs( this );
      this.EventManager.RaiseEvent( EditableRoutedEvents.RequestBeginEditEvent, e );

      return e.Result;
    }

    public bool EndEdit()
    {
      var e = new XceedRoutedEventRequestEditEventArgs( this );
      this.EventManager.RaiseEvent( EditableRoutedEvents.RequestEndEditEvent, e );

      return e.Result;
    }

    public bool CancelEdit()
    {
      var e = new XceedRoutedEventRequestEditEventArgs( this );
      this.EventManager.RaiseEvent( EditableRoutedEvents.RequestCancelEditEvent, e );

      return e.Result;
    }

    #endregion

    #region VisualState Methods

    private void SetEditingVisualState( string state, bool useTransitions )
    {
      this.VisualStateHelper.GoToEditingState( state, useTransitions );
    }

    private void SetValidationState( string state, bool useTransitions )
    {
      this.VisualStateHelper.GoToValidationState( state, useTransitions );
    }

    private void UpdateEditingVisualState()
    {
      CellEditingState state = ( m_viewModel != null )
                                 ? m_viewModel.EditingState
                                 : CellEditingState.Unchanged;

      switch( state )
      {
        case CellEditingState.Edited:
        case CellEditingState.Canceling:
          this.SetEditingVisualState( EditableCell.EditingStateEdited, true );
          break;

        case CellEditingState.Committing:
          this.SetEditingVisualState( EditableCell.EditingStateCommitting, true );
          break;

        default:
          this.SetEditingVisualState( EditableCell.EditingStateUnchanged, true );
          break;
      }
    }

    private void UpdateValidationVisualState()
    {
      if( this.HasValidationErrors )
      {
        this.ValidationState = Xceed.Silverlight.DataGrid.ValidationState.Invalid;
        this.SetValidationState( EditableCell.ValidationStateInvalid, true );
      }
      else
      {
        this.ValidationState = Xceed.Silverlight.DataGrid.ValidationState.Valid;
        this.SetValidationState( EditableCell.ValidationStateValid, true );
      }
    }

    #endregion

    public override void OnApplyTemplate()
    {
      base.OnApplyTemplate();

      FrameworkElement oldContainer = this.GetContentElement( m_contentHost );
      FrameworkElement newContainer = this.GetContentElement( m_contentElement );
      bool isNewContainer = ( oldContainer != newContainer );

      if( m_contentHost != null )
      {
        if( isNewContainer )
        {
          m_contentElementManager.Clear( oldContainer );
        }

        m_contentHost.Content = null;
      }

      m_contentHost = this.GetTemplateChild( "CellContentHost" ) as ContentControl;

      if( m_contentHost != null )
      {
        this.SetContentElement( newContainer, isNewContainer );
      }
    }

    protected override void OnKeyDown( KeyEventArgs e )
    {
      base.OnKeyDown( e );

      m_keyHandler.HandleRequest( e );
    }

    protected override void OnMouseLeftButtonDown( MouseButtonEventArgs e )
    {
      XceedEventUtils.RaiseMouseButtonEvent( this, e, EditableCell.EditableCellMouseLeftButtonDownEvent );
    }

    internal bool SetFocusOnContentElement()
    {
      var container = this.GetContentElement( m_contentElement ) as Control;
      if( container == null )
        return false;

      return container.Focus();
    }

    private bool SetFocusOnContentElementIfCellHasFocus()
    {
      if( CompatibilityUtils.GetFocusedElement( this ) != this )
        return false;

      return this.SetFocusOnContentElement();
    }

    private void UpdateContent()
    {
      object content = null;
      if( m_viewModel != null )
      {
        content = m_viewModel.Value;
      }

      if( this.EditedContent != content )
      {
        this.EditedContent = content;
      }
    }

    private void UpdateSource( object newValue )
    {
      if( ( m_viewModel == null ) || ( m_viewModel.Value == newValue ) )
        return;

      m_viewModel.Value = newValue;
      this.EventManager.RaiseEvent( EditableRoutedEvents.RequestPropertyChangedCommitEvent, new XceedRoutedEventArgs( this ) );
    }

    private void UpdateValidationErrors()
    {
      this.RaisePropertyChanged( () => this.ValidationErrors );
      this.RaisePropertyChanged( () => this.HasValidationErrors );
      this.RaisePropertyChanged( () => this.HasRestrictiveValidationErrors );

      this.UpdateValidationVisualState();
    }

    private FrameworkElement GetContentElement( ContentControl source )
    {
      if( source == null )
        return null;

      return source.Content as FrameworkElement;
    }

    private FrameworkElement GetContentElement( CellContentElement source )
    {
      if( source == null )
        return null;

      return source.Container;
    }

    private void RaiseActivatingCellEditor()
    {
      if( m_contentHost == null )
        return;

      if( ( m_contentElement == null ) || ( m_contentElement.ContainerType != EditableCellContentElementType.Editor ) )
        return;

      var container = this.GetContentElement( m_contentElement );
      this.EventManager.RaiseEvent( EditableRoutedEvents.ActivatingCellEditorEvent,
                                    new XceedRoutedEventActivatingCellEditorEventArgs( this, container ) );
    }

    private void SetContentElement( FrameworkElement container, bool raiseActivatingCellEditor )
    {
      if( m_contentHost == null )
        return;

      FrameworkElement oldContainer = this.GetContentElement( m_contentHost );
      FrameworkElement newContainer = container;
      bool isNewContainer = ( oldContainer != newContainer );

      if( isNewContainer )
      {
        m_contentHost.Content = newContainer;
      }

      if( raiseActivatingCellEditor )
      {
        this.RaiseActivatingCellEditor();

        m_contentElementManager.UpdateTargetBinding( newContainer );
      }
    }

    private void ContentElementReadyCallback( bool isReady )
    {
      if( !isReady )
        return;

      this.UpdateSource( this.EditedContent );
    }

    private void AttachContentElementReadyState( FrameworkElement container )
    {
      if( container == null )
        return;

      this.DetachContentElementReadyState( container );

      EditableCell.SetContentElementReadyState( container, new ContentElementReadyState( container, this.ContentElementReadyCallback ) );
    }

    private void DetachContentElementReadyState( FrameworkElement container )
    {
      if( container == null )
        return;

      ContentElementReadyState readyState = EditableCell.GetContentElementReadyState( container );
      if( readyState == null )
        return;

      readyState.Clear();
      EditableCell.ClearContentElementReadyState( container );
    }

    #region Private Fields

    private readonly KeyHandler m_keyHandler;
    private readonly ContentElementManager m_contentElementManager;
    private ContentControl m_contentHost;

    #endregion

    #region CellEditKeyHandler Nested Type

    private sealed class CellEditKeyHandler : EditKeyHandler
    {
      internal CellEditKeyHandler( EditableCell targetCell, KeyHandler successor )
        : base( successor )
      {
        if( targetCell == null )
          throw new ArgumentNullException( "targetCell" );

        m_targetCell = targetCell;
      }

      protected override bool BeginEdit()
      {
        bool result = m_targetCell.BeginEdit();
        m_targetCell.Dispatcher.BeginInvoke( () => m_targetCell.SetFocusOnContentElementIfCellHasFocus() );
        return result;
      }

      protected override bool EndEdit()
      {
        return m_targetCell.EndEdit();
      }

      protected override bool CancelEdit()
      {
        return m_targetCell.CancelEdit();
      }

      private readonly EditableCell m_targetCell;
    }

    #endregion

    #region ContentElementManager Nested Type

    private sealed class ContentElementManager : CellContentElementManager<EditableCell>
    {
      internal ContentElementManager( EditableCell targetCell )
        : base( targetCell )
      {
      }

      protected override void OnPrepare( FrameworkElement container )
      {
        base.OnPrepare( container );

        this.Owner.AttachContentElementReadyState( container );
      }

      protected override void OnClear( FrameworkElement container )
      {
        base.OnClear( container );

        this.Owner.DetachContentElementReadyState( container );
      }

      protected override bool TryGetContentSourceBinding( 
        FrameworkElement container, 
        DependencyProperty targetProperty, 
        out Binding binding )
      {
        BindingMode bindingMode = BindingMode.OneWay;

        var contentElement = this.Owner.ContentElement;
        if( ( contentElement != null ) && ( contentElement.ContainerType == EditableCellContentElementType.Editor ) )
        {
          bindingMode = BindingMode.TwoWay;
        }

        binding = this.CreateBinding(
                    ReflectionHelper.GetPropertyOrFieldName( () => this.Owner.Content ),
                    bindingMode );

        return ( binding != null );
      }

      protected override bool TryGetTextTrimmingSourceBinding(
        FrameworkElement container, 
        DependencyProperty targetProperty, 
        out Binding binding )
      {
        binding = this.CreateBinding(
                    ReflectionHelper.GetPropertyOrFieldName( () => this.Owner.TextTrimming ) );

        return ( binding != null );
      }
    }

    #endregion

    #region ContentElementReadyState Nested Type

    private sealed class ContentElementReadyState
    {
      internal ContentElementReadyState( FrameworkElement target, Action<bool> callback )
      {
        if( target == null )
          throw new ArgumentNullException( "target" );

        m_target = target;
        m_target.Loaded += new RoutedEventHandler( this.OnLoaded );
        m_target.Unloaded += new RoutedEventHandler( this.OnUnloaded );

        m_callback = callback;
      }

      internal bool IsReady
      {
        get
        {
          return m_isReady;
        }
        private set
        {
          if( m_isReady == value )
            return;

          m_isReady = value;

          if( m_callback != null )
          {
            m_callback.Invoke( value );
          }
        }
      }

      internal void Clear()
      {
        m_callback = null;

        m_target.Loaded -= new RoutedEventHandler( this.OnLoaded );
        m_target.Unloaded -= new RoutedEventHandler( this.OnUnloaded );
        m_target = null;

        m_isReady = false;
      }

      private void OnLoaded( object sender, RoutedEventArgs e )
      {
        this.IsReady = true;
      }

      private void OnUnloaded( object sender, RoutedEventArgs e )
      {
        this.IsReady = false;
      }

      private bool m_isReady;
      private FrameworkElement m_target;
      private Action<bool> m_callback;
    }

    #endregion
  }
}
